/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.sort;

import java.io.IOException;
import java.util.Comparator;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;

/**
 * Class to mediate with the sort configuration
 *
 * @author Richard Jones
 */
public class SortOption {
    private static final Logger log = LogManager.getLogger(SortOption.class);

    public static final String ASCENDING = "ASC";
    public static final String DESCENDING = "DESC";

    /**
     * the number of the sort option as given in the configuration file
     */
    private int number;

    /**
     * the name of the sort
     */
    private String name;

    /**
     * the metadata field to sort on
     */
    private String metadata;

    /**
     * the type of data we are sorting by
     */
    private String type;

    /**
     * the metadata broken down into bits for convenience
     */
    private String[] mdBits;

    /**
     * should the sort option be visible for user selection
     */
    private boolean visible;

    /**
     * the sort options available for this index
     */
    private static Set<SortOption> sortOptionsSet = null;

    static {
        try {
            Set<SortOption> newSortOptionsSet = new TreeSet<SortOption>(new Comparator<SortOption>() {
                @Override
                public int compare(SortOption sortOption, SortOption sortOption1) {
                    return Integer.valueOf(sortOption.getNumber()).compareTo(Integer.valueOf(sortOption1.getNumber()));
                }
            });
            int idx = 1;
            String option;

            ConfigurationService configurationService
                    = DSpaceServicesFactory.getInstance().getConfigurationService();
            while (((option = configurationService.getProperty("webui.itemlist.sort-option." + idx))) != null) {
                SortOption so = new SortOption(idx, option);
                newSortOptionsSet.add(so);
                idx++;
            }

            SortOption.sortOptionsSet = newSortOptionsSet;
        } catch (SortException se) {
            log.fatal("Unable to load SortOptions", se);
        }
    }

    /**
     * Construct a new SortOption object with the given parameters
     *
     * @param number the number of the sort option as given in the config file
     * @param name   sort option name
     * @param md     the metadata field to sort on
     * @param type   the type of data we are sorting by
     * @throws SortException if sort error
     */
    public SortOption(int number, String name, String md, String type)
        throws SortException {
        this.name = name;
        this.type = type;
        this.metadata = md;
        this.number = number;
        this.visible = true;
        generateMdBits();
    }

    /**
     * Construct a new SortOption object using the definition from the configuration
     *
     * @param number     the number of the sort option as given in the config file
     * @param definition definition from the configuration
     * @throws SortException if sort error
     */
    public SortOption(int number, String definition)
        throws SortException {
        this.number = number;

        String rx = "(\\w+):([\\w\\.\\*]+):(\\w+):?(\\w*)";
        Pattern pattern = Pattern.compile(rx);
        Matcher matcher = pattern.matcher(definition);

        if (!matcher.matches()) {
            throw new SortException("Sort Order configuration is not valid: webui.itemlist.sort-option." +
                                        number + " = " + definition);
        }

        name = matcher.group(1);
        metadata = matcher.group(2);
        type = matcher.group(3);

        // If the option is configured to be hidden, then set the visible flag to false
        // otherwise, flag it as visible (true)
        if (matcher.groupCount() > 3 && "hide".equalsIgnoreCase(matcher.group(4))) {
            visible = false;
        } else {
            visible = true;
        }

        generateMdBits();
    }

    /**
     * @return Returns the metadata.
     */
    public String getMetadata() {
        return metadata;
    }

    /**
     * @param metadata The metadata to set.
     */
    public void setMetadata(String metadata) {
        this.metadata = metadata;
    }

    /**
     * @return Returns the name.
     */
    public String getName() {
        return name;
    }

    /**
     * @param name The name to set.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return Returns the type.
     */
    public String getType() {
        return type;
    }

    /**
     * @param type The type to set.
     */
    public void setType(String type) {
        this.type = type;
    }

    /**
     * @return Returns the sort option number (as given in the config file).
     */
    public int getNumber() {
        return number;
    }

    /**
     * @param number The number to set.
     */
    public void setNumber(int number) {
        this.number = number;
    }

    /**
     * Should this sort option be made visible in the UI
     *
     * @return true if visible, false otherwise
     */
    public boolean isVisible() {
        return visible;
    }

    /**
     * @return a 3-element array of the metadata bits
     */
    public String[] getMdBits() {
        return (String[]) ArrayUtils.clone(mdBits);
    }

    /**
     * Tell the class to generate the metadata bits
     *
     * @throws SortException if sort error
     */
    private void generateMdBits()
        throws SortException {
        try {
            mdBits = interpretField(metadata, null);
        } catch (IOException e) {
            throw new SortException(e);
        }
    }

    /**
     * Take a string representation of a metadata field, and return it as an array.
     * This is just a convenient utility method to basically break the metadata
     * representation up by its delimiter (.), and stick it in an array, inserting
     * the value of the init parameter when there is no metadata field part.
     *
     * @param mfield the string representation of the metadata
     * @param init   the default value of the array elements
     * @return a 3-element array with schema, element and qualifier respectively
     * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations.
     */
    public final String[] interpretField(String mfield, String init)
        throws IOException {
        StringTokenizer sta = new StringTokenizer(mfield, ".");
        String[] field = {init, init, init};

        int i = 0;
        while (sta.hasMoreTokens()) {
            field[i++] = sta.nextToken();
        }

        // error checks to make sure we have at least a schema and qualifier for both
        if (field[0] == null || field[1] == null) {
            throw new IOException("at least a schema and element be " +
                                      "specified in configuration.  You supplied: " + mfield);
        }

        return field;
    }

    /**
     * Is this a date field?
     *
     * @return true if is date
     */
    public boolean isDate() {
        if ("date".equals(type)) {
            return true;
        }

        return false;
    }

    /**
     * Is the default sort option?
     *
     * @return true if is default sort option
     */
    public boolean isDefault() {
        if (number == 0) {
            return true;
        }
        return false;
    }

    /**
     * Return all the configured sort options.
     *
     * @return a set of the configured sort options
     * @throws SortException if sort error
     */
    public static Set<SortOption> getSortOptions() throws SortException {
        if (SortOption.sortOptionsSet == null) {
            throw new SortException("Sort options not loaded");
        }

        return SortOption.sortOptionsSet;
    }

    /**
     * Get the defined sort option by name
     *
     * @param name the name of the sort option as given in the config file
     * @return the configured sort option with given name
     * @throws SortException if sort error
     */
    public static SortOption getSortOption(String name) throws SortException {
        for (SortOption so : SortOption.getSortOptions()) {
            if (StringUtils.equals(name, so.getName())) {
                return so;
            }
        }

        return null;
    }

    /**
     * Get the defined sort option by number (.1, .2, etc).
     *
     * @param number the number of the sort option as given in the config file
     * @return the configured sort option with given number
     * @throws SortException if sort error
     */
    public static SortOption getSortOption(int number) throws SortException {
        for (SortOption so : SortOption.getSortOptions()) {
            if (so.getNumber() == number) {
                return so;
            }
        }

        return null;
    }

    /**
     * Get the default sort option - initially, just the first one defined.
     *
     * @return the first configured sort option
     * @throws SortException if sort error
     */
    public static SortOption getDefaultSortOption() throws SortException {
        for (SortOption so : getSortOptions()) {
            return so;
        }

        return null;
    }
}
